package com.zxjbyte.yiyi.framework.web.filter;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.crypto.symmetric.SM4;
import com.zxjbyte.yiyi.framework.common.cache.guava.impl.GuavaCache;
import com.zxjbyte.yiyi.framework.common.constant.Constant;
import com.zxjbyte.yiyi.framework.common.constant.Method;
import com.zxjbyte.yiyi.framework.common.domain.api.R;
import com.zxjbyte.yiyi.framework.common.exception.ServiceException;
import com.zxjbyte.yiyi.framework.common.util.JsonUtil;
import com.zxjbyte.yiyi.framework.common.util.ServletXUtil;
import com.zxjbyte.yiyi.framework.common.util.ValidationUtil;
import com.zxjbyte.yiyi.framework.web.config.ApiProperties;
import com.zxjbyte.yiyi.framework.web.config.YiyiWebProperties;
import com.zxjbyte.yiyi.framework.web.context.CryptoAlgorithm;
import com.zxjbyte.yiyi.framework.web.context.CryptoMode;
import com.zxjbyte.yiyi.framework.web.context.annotation.ApiCrypto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.ServletRequestPathUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 加解密过滤器
 *
 * @Author zhangxingjia
 * @Date 2025/10/31 17:34
 * @Version: 1.0
 */
@RequiredArgsConstructor
public class ApiCryptoFilter extends OncePerRequestFilter {

    private final ApiProperties apiProperties;
    private final YiyiWebProperties yiyiWebProperties;
    private final RequestMappingHandlerMapping requestMappingHandlerMapping;

    private final GuavaCache<CryptoAlgorithm, Object> cryptoCache = GuavaCache.newGuavaCache(3L);


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestPath = ServletXUtil.getRequestPath(request);
        // 全局或接口排除路径
        if (!apiProperties.getCrypto().getEnabled()
            || ServletXUtil.matchesAnyPattern(requestPath, yiyiWebProperties.getExcludePaths())
            || ServletXUtil.matchesAnyPattern(requestPath, apiProperties.getCrypto().getExcludePaths())) {
            filterChain.doFilter(request, response);
            return;
        }
        /**
         * 配置优先级：注解配置 > 全局默认配置
         * 1. 如果接口有 @ApiCrypto 注解：使用注解的配置
         * 2. 如果接口没有 @ApiCrypto 注解：使用 ApiProperties 中的全局默认配置
         * 3. 如果全局开关关闭：所有接口都不进行加解密
         *
         */
        // 加密模式
        CryptoMode cryptoMode;
        // 加密算法
        CryptoAlgorithm algorithm;
        ApiCrypto apiCrypto = getApiCryptoAnnotation(request);
        if(apiCrypto != null && apiCrypto.mode().isCryptoEnabled()){
            cryptoMode = apiCrypto.mode();
            algorithm = apiCrypto.algorithm();
        } else if(ServletXUtil.matchesAnyPattern(requestPath, apiProperties.getCrypto().getIncludePaths())) {
            cryptoMode = apiProperties.getCrypto().getDefaultMode();
            algorithm = apiProperties.getCrypto().getAlgorithm();
        } else {
            filterChain.doFilter(request, response);
            return;
        }
        Object cryptoInstance = null;
        // 解密请求
        if(cryptoMode.isRequestDecryptEnabled()){
            String method = request.getMethod();
            if(StrUtil.equalsAny(method, Method.POST, Method.PUT, Method.DELETE)){
                try{
                    cryptoInstance = getCryptoInstance(algorithm);
                    request = new ApiDecryptionRequestWrapper(request, cryptoInstance, algorithm);
                } catch (Exception e){
                    if (apiProperties.getCrypto().getDebugMode()) {
                        logger.warn("请求参数解密失败: " + e.getMessage(), e);
                    }
                    writeFailResponse(response, "请求解密失败");
                }
            }
        }

        HttpServletResponse wrappedResponse = response;

        // 如果需要响应加密，创建包装器
        if(cryptoMode.isResponseEncryptEnabled()){
            try {
                cryptoInstance = getCryptoInstance(algorithm);
                wrappedResponse = new ApiEncryptionResponseWrapper(response, cryptoInstance, algorithm);
            } catch (Exception e) {
                if (apiProperties.getCrypto().getDebugMode()) {
                    logger.warn("响应加密初始化失败: " + e.getMessage(), e);
                }
                writeFailResponse(response, "响应加密初始化失败");
                return;
            }
        }

        try {
            filterChain.doFilter(request, wrappedResponse);
        } finally {
            // 加密响应
            if(cryptoMode.isResponseEncryptEnabled() && wrappedResponse instanceof ApiEncryptionResponseWrapper){
                try {
                    ((ApiEncryptionResponseWrapper) wrappedResponse).finishResponse();
                } catch (Exception e) {
                    if (apiProperties.getCrypto().getDebugMode()) {
                        logger.warn("响应加密失败: " + e.getMessage(), e);
                    }
                    writeFailResponse(response, "响应加密失败");
                }
            }
        }
    }


    /**
     * 返回失败响应
     * @param response
     * @param message
     */
    private void writeFailResponse(HttpServletResponse response, String message) {
        R<?> result = R.fail(message);
        String json = JsonUtil.toJsonAsString(result);
        ServletXUtil.write(response, json, MediaType.APPLICATION_JSON_VALUE);
    }

    /**
     * 获取请求对应的 ApiCrypto 注解
     */
    private ApiCrypto getApiCryptoAnnotation(HttpServletRequest request) {
        try {
            // 兼容 SpringBoot 2.X 版本会报错的问题
            if (!ServletRequestPathUtils.hasParsedRequestPath(request)) {
                ServletRequestPathUtils.parseAndCache(request);
            }

            // 获取处理器执行链
            HandlerExecutionChain handlerExecutionChain = requestMappingHandlerMapping.getHandler(request);
            if (handlerExecutionChain == null) {
                return null;
            }

            Object handler = handlerExecutionChain.getHandler();
            if (!(handler instanceof HandlerMethod)) {
                return null;
            }

            HandlerMethod handlerMethod = (HandlerMethod) handler;

            // 优先获取方法上的注解
            ApiCrypto methodAnnotation = handlerMethod.getMethodAnnotation(ApiCrypto.class);
            if (methodAnnotation != null) {
                return methodAnnotation;
            }

            // 其次获取类上的注解
            return handlerMethod.getBeanType().getAnnotation(ApiCrypto.class);
        } catch (Exception e) {
            // 避免因获取处理器失败而影响正常请求
            if (apiProperties.getCrypto().getDebugMode()) {
                logger.warn("获取ApiCrypto注解失败: " + e.getMessage(), e);
            }
            return null;
        }
    }


    /**
     * 获取加密器实例
     */
    private Object getCryptoInstance(CryptoAlgorithm algorithm) {
        return cryptoCache.get(algorithm, () -> createCryptoInstance(algorithm));
    }

    /**
     * 创建加密器实例
     */
    private Object createCryptoInstance(CryptoAlgorithm algorithm) {
        try {
            switch (algorithm) {
                case AES:
                    ApiProperties.Crypto.AesConfig aesConfig = apiProperties.getCrypto().getAes();
                    String mode = aesConfig.getMode();
                    String padding = aesConfig.getPadding();
                    ValidationUtil.assertNotBlank(aesConfig.getSecretKey(), () -> "Secret key required for AES algorithm. Please configure it in: yiyi.api.crypto.aes.secret-key");

                    // 根据模式创建不同的AES实例
                    if ("CBC".equalsIgnoreCase(mode)) {
                        ValidationUtil.assertNotBlank(aesConfig.getIv(), () -> "IV parameter required for AES algorithm in CBC mode");

                        // 验证IV长度（SM4 CBC需要16字节IV）
                        byte[] ivBytes = aesConfig.getIv().getBytes(Constant.UTF_8);
                        ValidationUtil.assertTrue(ivBytes.length == 16, "AES IV must be exactly 16 bytes");
                        return new AES(mode, padding, aesConfig.getSecretKey().getBytes(Constant.UTF_8), aesConfig.getIv().getBytes(Constant.UTF_8));
                    } else if ("ECB".equalsIgnoreCase(mode)) {
                        return new AES(mode, padding, aesConfig.getSecretKey().getBytes(Constant.UTF_8));
                    } else {
                        throw new ServiceException(String.format("Unsupported AES mode: %s. Supported modes: CBC, ECB", mode));
                    }
                case RSA:
                    ApiProperties.Crypto.RsaConfig rsaConfig = apiProperties.getCrypto().getRsa();
                    ValidationUtil.assertFalse(StrUtil.isBlank(rsaConfig.getPublicKey()) || StrUtil.isBlank(rsaConfig.getPrivateKey()), "RSA algorithm requires both public key and private key. " +
                        "Please configure: yiyi.api.crypto.rsa.public-key and yiyi.api.crypto.rsa.private-key");
                    return new RSA(rsaConfig.getPadding(), rsaConfig.getPrivateKey(), rsaConfig.getPublicKey());
                case SM4:
                    ApiProperties.Crypto.Sm4Config sm4Config = apiProperties.getCrypto().getSm4();
                    String secretKey = sm4Config.getSecretKey();
                    ValidationUtil.assertNotBlank(sm4Config.getSecretKey(), () -> "Secret key required for SM4 algorithm. Please configure it in: yiyi.api.crypto.sm4.secret-key");

                    // 验证密钥长度（SM4固定使用128-bit密钥，需要16字节）
                    byte[] keyBytes = secretKey.getBytes(Constant.UTF_8);
                    ValidationUtil.assertTrue(keyBytes.length == 16, "SM4 key must be exactly 16 bytes");

                    if ("CBC".equalsIgnoreCase(sm4Config.getMode())) {
                        ValidationUtil.assertNotBlank(sm4Config.getIv(), () -> "IV parameter required for SM4 algorithm in CBC mode");

                        // 验证IV长度（SM4 CBC需要16字节IV）
                        byte[] ivBytes = sm4Config.getIv().getBytes(Constant.UTF_8);
                        ValidationUtil.assertTrue(ivBytes.length == 16, "SM4 IV must be exactly 16 bytes");
                        return new SM4(sm4Config.getMode(), sm4Config.getPadding(), keyBytes, ivBytes);
                    } else if ("ECB".equalsIgnoreCase(sm4Config.getMode())) {
                        return new SM4(sm4Config.getMode(), sm4Config.getPadding(), keyBytes);
                    } else {
                        throw new ServiceException(String.format("Unsupported SM4 mode: %s. Supported modes: CBC, ECB" ,sm4Config.getMode()));
                    }
                default:
                    throw new ServiceException(String.format("Unsupported encryption algorithm: %s. Supported algorithms: AES, RSA, SM4", algorithm));
            }
        } catch (Exception e) {
            if (apiProperties.getCrypto().getDebugMode()) {
                logger.warn("创建算法实例失败: " + e.getMessage(), e);
            }
            if(e instanceof ServiceException){
                throw (ServiceException) e;
            } else {
                throw new ServiceException(String.format("Failed to create crypto instance for algorithm: %s. Error: %s",
                    algorithm, e.getMessage()));
            }
        }
    }
}
